﻿using System;

using DDDSample.Common.Validate;
using DDDSample.Domain.Model.Handling;
using DDDSample.Domain.Model.Locations;

namespace DDDSample.Domain.Model.Cargos
{
	/// <summary>
	/// A Cargo. This is the central class in the domain model,
	/// and it is the root of the Cargo-Itinerary-Leg-DeliveryHistory aggregate.
	/// 
	/// A cargo is identified by a unique tracking id, and it always has an origin
	/// and a destination. The life cycle of a cargo begins with the booking procedure,
	/// when the tracking id is assigned. During a (short) period of time, between booking
	/// and initial routing, the cargo has no itinerary.
	/// 
	/// The booking clerk requests a list of possible routes, matching a route specification,
	/// and assigns the cargo to one route. An itinerary listing the legs of the route
	/// is attached to the cargo.
	/// 
	/// A cargo can be re-routed during transport, on demand of the customer, in which case
	/// the destination is changed and a new route is requested. The old itinerary,
	/// being a value object, is discarded and a new one is attached.
	/// 
	/// It may also happen that a cargo is accidentally misrouted, which should notify the proper
	/// personnel and also trigger a re-routing procedure.
	/// 
	/// The life cycle of a cargo ends when the cargo is claimed by the customer.
	/// 
	/// The cargo aggregate, and the entre domain model, is built to solve the problem
	/// of booking and tracking cargo. All important business rules for determining whether
	/// or not a cargo is misrouted, what the current status of the cargo is (on board carrier,
	/// in port etc), are captured in this aggregate.
	/// </summary>
	public class Cargo : Entity<Cargo>
	{
		private TrackingId trackingId;
		private Location origin;
		private Location destination;
		private Itinerary itinerary;
		private DeliveryHistory deliveryHistory;

		/// <summary>
		/// 
		/// </summary>
		/// <param name="trackingId">tracking id</param>
		/// <param name="origin">origin location</param>
		/// <param name="destination">destination location</param>
		public Cargo(TrackingId trackingId, Location origin, Location destination)
		{
			Validate.NotNull(trackingId);
			Validate.NotNull(origin);
			Validate.NotNull(destination);

			this.trackingId = trackingId;
			this.origin = origin;
			this.destination = destination;
		}

		/// <summary>
		/// The tracking id is the identity of this entity, and is unique.
		/// </summary>
		public TrackingId TrackingId
		{
			get { return this.trackingId; }
		}

		/// <summary>
		/// Origin location.
		/// </summary>
		public Location Origin
		{
			get { return this.origin; }
		}

		/// <summary>
		/// Destination of the cargo.
		/// </summary>
		public Location Destination
		{
			get { return this.destination; }
		}

		/// <summary>
		/// The delivery history. Never null.
		/// </summary>
		public DeliveryHistory DeliveryHistory
		{
			get { return this.deliveryHistory ?? DeliveryHistory.EMPTY_DELIVERY_HISTORY; }
			set
			{
				Validate.NotNull(value);
				this.deliveryHistory = value;
			}
		}

		/// <summary>
		/// The itinerary. Never null.
		/// </summary>
		public Itinerary Itinerary
		{
			get { return this.itinerary ?? Itinerary.EMPTY_ITINERARY; }
		}

		/// <summary>
		/// Last known location of the cargo, or Location.UNKNOWN if the delivery history is empty.
		/// </summary>
		public Location LastKnownLocation
		{
			get
			{
				HandlingEvent lastEvent = DeliveryHistory.LastEvent;
				if (lastEvent != null)
				{
					return lastEvent.Location;
				}
				else
				{
					return Location.UNKNOWN;
				}
			}
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="newDestination">the new destination. May not be null.</param>
		public void ChangeDestination(Location newDestination)
		{
			Validate.NotNull(newDestination);

			this.destination = newDestination;
		}

		/// <summary>
		/// True if the cargo has arrived at its final destination.
		/// </summary>
		/// <returns></returns>
		public bool HasArrived()
		{
			return destination.Equals(LastKnownLocation);
		}

		/// <summary>
		/// Attach a new itinerary to this cargo.
		/// </summary>
		/// <param name="itinerary">an itinerary. May not be null.</param>
		public void AttachItinerary(Itinerary itinerary)
		{
			Validate.NotNull(itinerary);

			// Decouple the old itinerary from this cargo
			Itinerary.Cargo = null;

			// Couple this cargo and the new itinerary
			this.itinerary = itinerary;
			this.itinerary.Cargo = this;
		}

		/// <summary>
		/// Detaches the current itinerary from the cargo.
		/// </summary>
		public void DetachItinerary()
		{
			Itinerary.Cargo = null;
			this.itinerary = null;
		}

		/// <summary>
		/// Check if cargo is misdirected.
		/// <p/>
		/// <ul>
		/// <li>A cargo is misdirected if it is in a location that's not in the itinerary.
		/// <li>A cargo with no itinerary can not be misdirected.
		/// <li>A cargo that has received no handling events can not be misdirected.
		/// </ul>
		/// </summary>
		/// <returns>true if the cargo has been misdirected</returns>
		public bool IsMisdirected()
		{
			HandlingEvent lastEvent = DeliveryHistory.LastEvent;
			if (lastEvent == null)
			{
				return false;
			}
			else
			{
				return !itinerary.IsExpected(lastEvent);
			}
		}

		/// <summary>
		/// Does not take into account the possibility of the cargo having been
		/// (errouneously) loaded onto another carrier after it has been unloaded
		/// at the final destination.
		/// </summary>
		/// <returns>True if the cargo has been unloaded at the final destination.</returns>
		public bool IsUnloadedAtDestination()
		{
			foreach (HandlingEvent @event in DeliveryHistory.EventsOrderedByCompletionTime)
			{
				if (@event.Type == HandlingEvent.EventType.Unload &&
					Destination.Equals(@event.Location))
				{
					return true;
				}
			}

			return false;
		}

		public override bool SameIdentityAs(Cargo other)
		{
			return other != null && this.trackingId.SameValueAs(other.trackingId);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="obj">object to compare</param>
		/// <returns>True if they have the same identity</returns>
		public override bool Equals(object obj)
		{
			if (!(obj is Cargo))
			{
				return false;
			}

			Cargo other = (Cargo)obj;
			return SameIdentityAs(other);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns>Hash code of tracking id.</returns>
		public override int GetHashCode()
		{
			return this.trackingId.GetHashCode();
		}

		internal Cargo()
		{
			// Needed by Hibernate
		}

		// Auto-generated surrogate key
		private long id;
	}
}
